// Copyright (C) 2010 David Sugar, Tycho Softworks.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include <ucommon/secure.h>
#include <ctype.h>
#include <sys/stat.h>

using namespace UCOMMON_NAMESPACE;

static shell::flagopt helpflag('h',"--help",    _TEXT("display this list"));
static shell::flagopt althelp('?', NULL, NULL);
static shell::numericopt bufopt('b', "--buffer", _TEXT("size of buffer"), "xk");
static shell::flagopt ignore('i', "--ignore", _TEXT("ignore errors"));
static shell::flagopt no('n', "--keep", _TEXT("never overwrite"));
static shell::flagopt follow('F', "--follow", _TEXT("follow symlinks"));
static shell::flagopt recursive('R', "--recursive", _TEXT("recursive directory scan"));
static shell::flagopt altrecursive('r', NULL, NULL);
static shell::flagopt yes('y', "--overwrite", _TEXT("overwrite files"));

static int exit_code = 0;
static const char *argv0 = "mv";
static char *buffer;
static size_t bufsize = 4096;

static void report(const char *path, int code)
{
    const char *err = _TEXT("i/o error");

    switch(code) {
    case EACCES:
    case EPERM:
        err = _TEXT("permission denied");
        break;
    case EROFS:
        err = _TEXT("read-only file system");
        break;
    case ENODEV:
    case ENOENT:
        err = _TEXT("no such file or directory");
        break;
    case ENOTDIR:
        err = _TEXT("not a directory");
        break;
    case ENOTEMPTY:
        err = _TEXT("directory not empty");
        break;
    case ENOSPC:
        err = _TEXT("no space left on device");
        break;
    case EBADF:
    case ENAMETOOLONG:
        err = _TEXT("bad file path");
        break;
    case EBUSY:
    case EINPROGRESS:
        err = _TEXT("file or directory busy");
        break;
    case EINTR:
        err = _TEXT("operation interupted");
        break;
#ifdef  ELOOP
    case ELOOP:
        err = _TEXT("too many sym links");
        break;
#endif
    }

    if(is(ignore))
        shell::printf(" - %s\n", err);
    else
        shell::errexit(1, "*** %s: %s: %s\n", argv0, path, err);

    exit_code = 1;
}

static const char *filename(const char *path)
{
    const char *cp = strrchr(path, '/');
    const char *cp1 = strrchr(path, '\\');
    if(cp1 > cp)
        cp = cp1;

    if(cp)
        return ++cp;

    return path;
}

static void makepath(const char *path)
{
    string_t dir = path;
    char *cp, *cp1;
    char *base = (char *)(*dir);
    if(base[1] == ':')
        base += 3;
    else
        ++base;

    while(base && *base) {
        cp = strrchr(base, '\\');
        cp1 = strrchr(base, '/');
        if(cp1 && cp1 < cp)
            cp = cp1;
        if(!cp)
            break;

        *cp = 0;
        dir::create(*dir, fsys::OWNER_PUBLIC);
        *cp = '/';
        base = ++cp;
    }
    dir::create(*dir, fsys::OWNER_PUBLIC);
}

static void movefile(const char *src, const char *dest)
{
    fsys_t input, output;

    if(fsys::is_file(dest) && !is(yes)) {
        string_t prompt = str("replace ") + str(dest) + str(" <y/n>? ");
        if(is(no))
            return;
        switch(shell::inkey(prompt)) {
        case 'y':
        case 'Y':
            printf("y\n");
            break;
        default:
            printf("n\n");
        case 0:
            return;
        }
    }

    if(fsys::is_link(src)) {
        report(src, fsys::linkinfo(src, buffer, sizeof(buffer)));
        report(dest, fsys::link(buffer, dest));
        report(src, fsys::unlink(src));
        return;
    }

    if(!::rename(src, dest)) {
        ::remove(src);  // in case was hardlinked..
        return;
    }

    input.open(src, fsys::STREAM);
    if(!is(input)) {
        report(src, input.err());
        return;
    }

    output.open(dest, 0644, fsys::WRONLY);
    if(!is(output)) {
        report(dest, output.err());
        return;
    }

    ssize_t iosize = bufsize;
    while(iosize > 0) {
        iosize = input.read(buffer, bufsize);
        if(iosize < 0) {
            report(src, input.err());
            iosize = 0;
        }
        if(iosize > 0)
            iosize = output.write(buffer, iosize);
        if(iosize < 0)
            report(dest, output.err());
    }
    ::remove(src);
}

static void scan(string_t& path, string_t& target)
{
    char filename[128];
    dir_t dir(*path);

    while(is(dir) && dir.read(filename, sizeof(filename))) {
        if(*filename == '.' && (filename[1] == '.' || !filename[1]))
            continue;

        string_t filepath = path + str("/") + str(filename);
        string_t nextpath = target + str("/") + str(filename);

        if(fsys::is_dir(*filepath)) {
            if(!fsys::is_link(*filepath) || is(follow)) {
                if(dir::create(*nextpath, fsys::OWNER_PUBLIC))
                    scan(filepath, nextpath);
            }
            else
                ::remove(*filepath);
        }
        else
            movefile(*filepath, *nextpath);
    }
    ::remove(*path);
}

PROGRAM_MAIN(argc, argv)
{
    shell::bind("mv");
    shell args(argc, argv);
    argv0 = args.argv0();
    unsigned index = 0;
    bool skel = false;

    argv0 = args.argv0();

    if(is(helpflag) || is(althelp)) {
        printf("%s: %s %s\n", _TEXT("Usage"), argv0, _TEXT("[options] file(s).. target"));
        printf("%s\n\n", _TEXT("Copy source files to target"));
        printf("%s\n", _TEXT("Options:"));
        shell::help();
        printf("\n%s\n", _TEXT("Report bugs to dyfet@gnu.org"));
        PROGRAM_EXIT(0);
    }

    if(is(bufopt))
        bufsize = *bufopt * 1024l;

    if(bufsize < 1)
        shell::errexit(2, "*** %s: %ld: %s\n", argv0, (long)bufsize,
            _TEXT("invalid buffer size"));

    buffer = new char[bufsize];
    if(!buffer)
        shell::errexit(2, "*** %s: %ld: %s\n", argv0, (long)bufsize,
            _TEXT("cannot allocate buffer"));

    fsys_t output(shell::output());

    if(!args())
        shell::errexit(1, "*** %s: %s\n", argv0,
            _TEXT("no files specified"));

    if(args() == 1)
        shell::errexit(1, "*** %s: %s\n", argv0,
            _TEXT("no target specified"));

    string_t target = str(args[args() - 1]);
    string_t source = args[0];

    char *end = (char *)(*target + strlen(*target) - 1);
    if(*end == '/' || *end == '\\') {
        skel = true;
        *end = 0;
    }

    if(!fsys::is_dir(*source) && args() == 2) {
        if(fsys::is_dir(*target)) {
            if(skel) {
                makepath(target);
                target = target + "/" + filename(*source);
            }
            else
                shell::errexit(2, "*** %s: %s: %s\n", argv0, *source,
                    _TEXT("Is a directory"));
        }
        else
            makepath(*target);
        movefile(*source, *target);
    }

    dir::create(*target, fsys::OWNER_PUBLIC);
    if(!fsys::is_dir(*target))
        shell::errexit(2, "*** %s: %s: %s\n", argv0, *target,
            _TEXT("Not a directory"));

    unsigned count = args() - 1;
    string_t dir;

    while(index < count) {
        source ^= args[index++];
        dir = target + "/" + filename(*source);
        if(fsys::is_dir(*source)) {
            if(!is(recursive) && !is(altrecursive) && !is(follow))
                fprintf(stderr, "*** %s: %s: %s\n", argv0, *source,
                    _TEXT("Is a directory"));
            else {
                if(dir::create(*dir, fsys::OWNER_PUBLIC))
                    scan(source, dir);
            }
            continue;
        }
        movefile(*source, *dir);
    }

    PROGRAM_EXIT(exit_code);
}

